package br.unifor.g2cl;

import java.net.SocketAddress;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.sf.jgcs.Message;


/**
 * 
 * Represents a messages transmitted by G2CL services.
 * 
 * <div style="text-align:justify;">
 * As it is typical with other communication abstractions that encapsulate lower-level services, 
 * to implement some of the G2CL services we needed a way to add service-specific headers to 
 * application messages in a manner that is separate from their body. Such headers would be used 
 * to store control information relevant to the implementation of some services, but which could 
 * not be exposed to the application. Since this facility is not readily supported by the Message 
 * class currently provided by jGCS, we had to define a new message class.
 * </div>
 * <br/>
 * <div style="text-align:justify;">
 * In order to maintain compatibility with the DataSession classe of jGCS, G2CLMessage implements 
 * jGCS's Message interface. This allows G2CLMessage objects to be transmitted as any other message 
 * using any jGCS plug-in.
 * </div>
 * 
 * @author Henrique Teofilo (henriquetft@gmail.com)
 * @author Leandro Sales (leandro.shp@gmail.com)
 */
public class G2CLMessage implements Message {
	
	private static Logger logger = Logger.getLogger(G2CLMessage.class.getName());
	
	// TODO extinguish it
	private static final byte HEADER = (byte) 0xA;
	
	/** Buffer used to store both payload and headers */
	private byte[] buffer;
	
	/** Cursor position */
	private int cursor;

	/** Sender address */
	private SocketAddress sender;
	
	/**
	 * Extra bytes added to the new length of buffer to speed up the next
	 * addHeader calls (so that the grow method don't need to be called).
	 * 
	 * @see #grow(int)
	 */
	private static final int FOLGA = 256;
	
	public static G2CLMessage fromMessage(Message templateMessage){
		G2CLMessage message = new G2CLMessage();
		message.setPayload(templateMessage.getPayload());
		message.setSenderAddress(templateMessage.getSenderAddress());
		return message;
	}
	
	
	/* ********************************************************************** *
	 * ***************** Implementation of Message Interface **************** *
	 * ********************************************************************** */
	
	/**
	 * Sets the payload for the message.
	 * @param payload payload
	 */
	public void setPayload(byte[] payload) {
		if (payload == null) {
			payload = new byte[0];
		}
		this.buffer = payload;
		this.cursor = 0;
	}
	
	/**
	 * Returns a copy of payload.
	 * May bring headers, depending on the cursor position.
	 * @return Copy of payload 
	 */
	public byte[] getPayload() {
		if (cursor == 0) {
			return buffer;
		} else {
			byte[] ret = new byte[size()];
			System.arraycopy(buffer, cursor, ret, 0, ret.length);
			return ret;
		}
	}

	public SocketAddress getSenderAddress() {
		return sender;
	}

	public void setSenderAddress(SocketAddress sender) {
		this.sender = sender;
	}
	
	
	/* ********************************************************************** *
	 * ******* Implementation of the new methods to add/remove headers ****** *
	 * ********************************************************************** */
	
	/**
	 * Adds a header onto the top
	 * @param header to be added
	 */
	public void addHeader(byte[] header) {
		// TODO change the name of this method
		if (buffer == null) {
			// a inesquecível exceção: PAYLOAD PRIMEIRO
			throw new IllegalStateException("Payload must be added first");
		}
		
		// verify capacity (5 means int for the length of header plus HEADER)
		int HEADER_LENGTH_PLUS_HEADER_CONTENT = 5;
		
		if (cursor - HEADER_LENGTH_PLUS_HEADER_CONTENT - header.length < 0) {
			grow(header.length + HEADER_LENGTH_PLUS_HEADER_CONTENT);
		}
		
		// copying header to buffer
		System.arraycopy(header, 0, buffer, cursor - header.length, header.length);

		cursor = cursor - header.length;

		int headerLen = header.length;
		
		//byte[] headerLenBytes = Util.intToByteArray(headerLen);

		// putting the length of buffer and header in the buffer
		buffer[--cursor] = (byte) (0xFF & headerLen);
		buffer[--cursor] = (byte) (0xFF & (headerLen >> 8));
		buffer[--cursor] = (byte) (0xFF & (headerLen >> 16));
		buffer[--cursor] = (byte) (0xFF & (headerLen >> 24));
		buffer[--cursor] = HEADER;
	}

	
	/** Removes and returns a header from this message.
	 * @return A new byte array containing the header at the top
	 */
	public byte[] removeHeader() {
		// TODO change the name of this method
		if (buffer == null || buffer[cursor] != HEADER) {
			throw new IllegalStateException("There is no headers");
		}
		
		int arrLen = ((int) buffer[cursor + 4] & 0xFF) | 
		(((int) buffer[cursor + 3] & 0xFF) << 8) |
		(((int) buffer[cursor + 2] & 0xFF) << 16) |
		(((int) buffer[cursor + 1] & 0xFF) << 24);

		cursor += 5;

		byte[] ret = new byte[arrLen];
		System.arraycopy(buffer, cursor, ret, 0, arrLen);
		cursor += arrLen; // just position the cursor to forget the header

		return ret;
	}

	// TODO change the name of this method to length()
	public int size() {
		return buffer.length - cursor;
	}
	
	/**
	 * Increases buffer length in the units specified to fit more headers.
	 * @param units Number of new positions buffer needs to have
	 */
	private void grow(int units) {
		int newSize = units + buffer.length + FOLGA;

		if (logger.isLoggable(Level.FINER)) {
			logger.finer("Increasing buffer. The new size is: " + newSize);
		}
		
		byte[] newHeader = new byte[newSize];

		int newPos = newSize - (buffer.length - cursor);
		System.arraycopy(buffer, cursor, newHeader, newPos, buffer.length - cursor);
		
		this.buffer = newHeader;
		this.cursor = newPos;
	}

	
	
	@Override
	public String toString() {
		StringBuilder msgAsStr = new StringBuilder();
		byte[] payload = getPayload();
		msgAsStr.append("[").append(getSenderAddress()).append(", SIZE: ")
				.append(payload.length).append("]");
		return msgAsStr.toString();
	}	
}